package org.estela.stats;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.logging.Logger;

/**
 * This is a simple bag for min, max, mean, variance and stddev values. 
 * This class contains also some static helper methods for mean, median 
 * and standard deviation.
 * 
 * @version 1.00
 * @date Apr 22, 2006
 * @author <a href="mailto:akantek@nyse.com">Antonio Kantek</a>
 */
public class BasicStatistics implements Serializable, Cloneable {
	private static final long serialVersionUID = 1L;

	/** Average of all the items that have been entered */
	private double _mean;

	/** standard deviation */
	private double _stddev;

	/** variance */
	private double _variance;

	/** min and max */
	private double _min, _max;

	/** Number of numbers that have been entered */
	private long _count;

	/** Sum of all the items that have been entered */
	private double _sum;

	/** dirty */
	private boolean _dirty;

	/** Square sum of all values */
	private double _sumSquare;

	/**
	 * Createas a new <code>Stats</code>.
	 */
	public BasicStatistics() {
		this.reset();
	}

	/**
	 * Createas a new <code>Stats</code>.
	 * 
	 * @param mean
	 * @param stddev
	 * @param variance
	 * @param min
	 * @param max
	 * @param count
	 * @param sum
	 */
	public BasicStatistics(double mean, double stddev, double variance, double min,
			double max, long count, double sum) {
		_mean = mean;
		_stddev = stddev;
		_variance = variance;
		_min = min;
		_max = max;
		_sum = sum;
		_count = count;
		_dirty = true;
	}

	/**
	 * Reset all statistics.
	 */
	public void reset() {
		_mean = Double.NaN;
		_stddev = Double.NaN;
		_variance = Double.NaN;
		_min = Double.NaN;
		_max = Double.NaN;
		_sum = 0;
		_count = 0;
		_sumSquare = 0;
	}

	/**
	 * @return Returns the max.
	 */
	public double getMax() {
		return _max;
	}

	/**
	 * @return Returns the mean.
	 */
	public double getMean() {
		if (_dirty) {
			this.calcueStats();
		}
		return _mean;
	}

	/**
	 * @return Returns the min.
	 */
	public double getMin() {
		return _min;
	}

	/**
	 * @return Returns the stddev.
	 */
	public double getStddev() {
		if (_dirty) {
			this.calcueStats();
		}
		return _stddev;
	}

	/**
	 * @return Returns the variance.
	 */
	public double getVariance() {
		if (_dirty) {
			this.calcueStats();
		}
		return _variance;
	}

	/**
	 * @return Returns the count.
	 */
	public long getCount() {
		return _count;
	}

	/**
	 * @return Returns the sum.
	 */
	public double getSum() {
		return _sum;
	}

	/**
	 * Compute stats.
	 */
	private void calcueStats() {
		if (_count == 0) {
			throw new IllegalStateException("Stats bag empty");
		}
		// calcule mean
		_mean = _sum / _count;

		// calcule variance
		_variance = _sumSquare - (_sum * _sum) / _count;
		_variance /= (_count - 1);

		if (_variance < 0) {
			Logger.getLogger(getClass().getName()).warning(
					"variance/stddev value = " + _variance
							+ " rounded to zero.");
			_variance = 0;
		}
		_stddev = Math.sqrt(_variance);
		_dirty = false;
	}

	@Override
	public int hashCode() {
		if (_dirty) {
			this.calcueStats();
		}

		int hash = 11;
		hash = (int) (_mean * _stddev * _variance * _min * _max * _count * _sum);

		return hash;
	}

	@Override
	public boolean equals(Object obj) {
		if (_dirty) {
			this.calcueStats();
		}

		if (obj instanceof BasicStatistics) {
			BasicStatistics other = (BasicStatistics) obj;
			if (other._dirty) {
				other.calcueStats();
			}
			return _mean == other._mean && _stddev == other._stddev
					&& _variance == other._variance && _min == other._min
					&& _max == other._max && _count == other._count
					&& _sum == other._sum;
		}
		return false;
	}

	@Override
	public String toString() {
		if (_dirty) {
			this.calcueStats();
		}
		DecimalFormat formatter = new DecimalFormat("#.###");
		StringBuilder buff = new StringBuilder();
		buff.append("[mean: ").append(formatter.format(_mean));
		buff.append(", variance: ").append(formatter.format(_variance));
		buff.append(", stddev: ").append(formatter.format(_stddev));
		buff.append(", min: ").append(formatter.format(_min));
		buff.append(", max: ").append(formatter.format(_max));
		buff.append(", count: ").append(formatter.format(_count));
		buff.append(", sum: ").append(formatter.format(_sum));
		buff.append("]");
		return buff.toString();
	}

	@Override
	public Object clone() {
		BasicStatistics clone = null;
		try {
			clone = (BasicStatistics) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new Error(e);
		}
		return clone;
	}

	/**
	 * Subtracts a value n times from this Stats object. Double.NaN are ignored.
	 * 
	 * @param value
	 *            Value to be subtracted
	 * @param n -
	 *            times
	 */
	public void substract(double value, int n) {
		if (value == Double.NaN) {
			return; // ignored
		}
		if (Double.isNaN(_min)) {
			throw new Error("Stats bag not initialized");
		}
		_sum -= value * n;
		_sumSquare -= value * value * n;
		_count -= n;
		_dirty = true;
	}

	/**
	 * Subtracts a value from this Stats object. Double.NaN are ignored.
	 * 
	 * @param value
	 *            Value to be subtracted
	 */
	public void subtract(double value) {
		this.substract(value, 1);
	}

	/**
	 * Adds a value n times to this Stats object. Double.NaN are ignored.
	 * 
	 * @param value -
	 *            Value to be added.
	 * @param n -
	 *            times
	 */
	public void add(double value, int n) {
		if (value == Double.NaN) {
			return; // ignored
		}
		_sum += value * n;
		_sumSquare += value * value * n;
		_count += n;
		if (Double.isNaN(_min)) {
			_min = _max = value;
		} else if (value < _min) {
			_min = value;
		} else if (value > _max) {
			_max = value;
		}
		_dirty = true;
	}

	/**
	 * Adds a new value to this Stats object. Double.NaN are ignored.
	 * 
	 * @param value -
	 *            value to be added.
	 */
	public void add(double value) {
		this.add(value, 1);
	}

	// Mean//////////////////////////////////////////////////////////////////////
	/**
	 * Computes the mean (the average) of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the mean
	 */
	public static double mean(int[] values) {
		double avg = 0;
		for (int i = 0; i < values.length; i++) {
			avg += values[i];
		}
		return avg / values.length;
	}

	/**
	 * Computes the mean (the average) of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the mean
	 */
	public static double mean(long[] values) {
		double avg = 0;
		for (int i = 0; i < values.length; i++) {
			avg += values[i];
		}
		return avg / values.length;
	}

	/**
	 * Computes the mean (the average) of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the mean
	 */
	public static double mean(float[] values) {
		double avg = 0;
		for (int i = 0; i < values.length; i++) {
			avg += values[i];
		}
		return avg / values.length;
	}

	/**
	 * Computes the mean (the average) of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the mean
	 */
	public static double mean(double[] values) {
		double avg = 0;
		for (int i = 0; i < values.length; i++) {
			avg += values[i];
		}
		return avg / values.length;
	}

	// Median////////////////////////////////////////////////////////////////////
	/**
	 * Returns the median of the elements contained in the array referred by the
	 * parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the median
	 */
	public static double median(int[] values) {
		int[] temp = new int[values.length];
		System.arraycopy(values, 0, temp, 0, values.length);
		Arrays.sort(temp);
		int length = temp.length / 2;
		return (values.length % 2 == 0) ? (temp[length] + temp[length - 1]) / 2
				: temp[length];
	}

	/**
	 * Returns the median of the elements contained in the array referred by the
	 * parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the median
	 */
	public static double median(long[] values) {
		long[] temp = new long[values.length];
		System.arraycopy(values, 0, temp, 0, values.length);
		Arrays.sort(temp);
		int length = temp.length / 2;
		return (values.length % 2 == 0) ? (temp[length] + temp[length - 1]) / 2
				: temp[length];
	}

	/**
	 * Returns the median of the elements contained in the array referred by the
	 * parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the median
	 */
	public static double median(float[] values) {
		float[] temp = new float[values.length];
		System.arraycopy(values, 0, temp, 0, values.length);
		Arrays.sort(temp);
		int length = temp.length / 2;
		return (values.length % 2 == 0) ? (temp[length] + temp[length - 1]) / 2
				: temp[length];
	}

	/**
	 * Returns the median of the elements contained in the array referred by the
	 * parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the median
	 */
	public static double median(double[] values) {
		double[] temp = new double[values.length];
		System.arraycopy(values, 0, temp, 0, values.length);
		Arrays.sort(temp);
		int length = temp.length / 2;
		return (values.length % 2 == 0) ? (temp[length] + temp[length - 1]) / 2
				: temp[length];
	}

	// STDDEV////////////////////////////////////////////////////////////////////
	/**
	 * Computes the standard deviation of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the standard deviation
	 */
	public static double stddev(int[] values) {
		double std = 0;
		double avg = mean(values);
		for (int i = 0; i < values.length; i++) {
			std += (values[i] - avg) * (values[i] - avg);
		}

		std /= values.length - 1;
		return Math.sqrt(std);
	}

	/**
	 * Computes the standard deviation of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the standard deviation
	 */
	public static double stddev(long[] values) {
		double std = 0;
		double avg = mean(values);
		for (int i = 0; i < values.length; i++) {
			std += (values[i] - avg) * (values[i] - avg);
		}

		std /= values.length - 1;
		return Math.sqrt(std);
	}

	/**
	 * Computes the standard deviation of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the standard deviation
	 */
	public static double stddev(float[] values) {
		double std = 0;
		double avg = mean(values);
		for (int i = 0; i < values.length; i++) {
			std += (values[i] - avg) * (values[i] - avg);
		}

		std /= values.length - 1;
		return Math.sqrt(std);
	}

	/**
	 * Computes the standard deviation of the elements contained in the array
	 * referred by the parameter.
	 * 
	 * @param values
	 *            Array of values
	 * @return the standard deviation
	 */
	public static double stddev(double[] values) {
		double std = 0;
		double avg = mean(values);
		for (int i = 0; i < values.length; i++) {
			std += (values[i] - avg) * (values[i] - avg);
		}

		std /= values.length - 1;
		return Math.sqrt(std);
	}
}